Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Jira Issue: NUGUDI-181
|
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough프로필 편집 기능을 구현하였습니다. 사용자 엔티티에 키와 체중 필드를 추가하고, 폼 유효성 검사 스키마를 정의한 후, 기본 정보와 체력 정보 입력 폼 컴포넌트를 작성하여 프로필 편집 뷰로 통합했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Form as 프로필 편집 폼
participant Validation as Zod 검증
participant Entity as UserEntity
participant Router as 라우터
User->>Form: 닉네임/키/체중 입력
Form->>Form: 필드 변경 감지
Form->>Validation: 폼 데이터 검증
alt 검증 성공
Validation-->>Form: 유효함
User->>Form: 수정 완료 버튼 클릭
Form->>Entity: 프로필 데이터 저장
Entity-->>Form: 저장 완료
Form->>Router: /profile로 이동
Router-->>User: 프로필 페이지 표시
else 검증 실패
Validation-->>Form: 오류 메시지
Form->>User: 유효성 검사 오류 표시
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 분
Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @hijjoy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 사용자 프로필 수정 기능을 도입하고, 기존 프로필 및 포인트 관련 UI를 전반적으로 개선합니다. 사용자 엔티티에 키와 몸무게 필드를 추가하고, 프로필 편집을 위한 유효성 검사 스키마를 정의하며, 관련 UI 컴포넌트를 새로 추가하거나 수정하여 사용자 경험을 향상시키는 것을 목표로 합니다. 서버 API가 아직 준비되지 않아 UI/프레젠테이션 계층에 집중하여 구현되었습니다. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts
Outdated
Show resolved
Hide resolved
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx
Show resolved
Hide resolved
.../web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx
Show resolved
Hide resolved
.../web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx
Outdated
Show resolved
Hide resolved
|
This pull request (commit
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/src/domains/user/domain/entities/user.entity.ts (1)
33-40:toPlainObject반환 타입이 구현과 일치하지 않습니다.인터페이스의
toPlainObject반환 타입(33-40줄)에는height와weight가 포함되어 있지 않지만, 실제 구현(240-241줄)에서는 이 필드들을 반환하고 있습니다. 이는 TypeScript 타입 안정성 문제를 일으킵니다.다음 diff를 적용하여 인터페이스의 반환 타입을 수정하세요:
toPlainObject(): { id: number; nickname: string; email?: string; profileImageUrl?: string; + height?: number; + weight?: number; createdAt?: string; updatedAt?: string; };Also applies to: 240-241
🧹 Nitpick comments (5)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts (2)
8-14: 공백 검증 로직이 중복됩니다.Line 8-11의 정규식
/^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]+$/이 이미 공백을 허용하지 않으므로, Line 12-14의.refine()검증은 불필요합니다.다음과 같이 refine을 제거할 수 있습니다:
nickname: z .string() .min(2, "닉네임은 최소 2자 이상이어야 합니다") .max(10, "닉네임은 최대 10자까지 가능합니다") .regex( /^[a-zA-Z0-9가-힣ㄱ-ㅎㅏ-ㅣ]+$/, "닉네임은 영문, 한글, 숫자만 사용할 수 있습니다", - ) - .refine((val) => !val.includes(" "), { - message: "닉네임에 공백을 포함할 수 없습니다", - }), + ),
15-16: 신체 정보 필드에 대한 검증 추가를 고려해 주세요.
height와weight에 대한 범위 검증이 없습니다. 실제 운영 환경에서는 음수나 비현실적인 값을 방지하기 위한 검증이 필요할 수 있습니다.예시:
height: z.number().positive().min(100).max(250).optional(), weight: z.number().positive().min(20).max(300).optional(),또는 Zod 4의 새로운 통합 에러 커스터마이징을 활용할 수도 있습니다:
height: z.number({ error: (issue) => { if (issue.code === 'too_small') return '키는 100cm 이상이어야 합니다'; if (issue.code === 'too_big') return '키는 250cm 이하여야 합니다'; } }).positive().min(100).max(250).optional(),apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx (1)
32-35: 실제 API 연동 시 수정이 필요합니다.
console.log와 단순 라우팅만 수행하고 있습니다. 실제 프로필 업데이트 API 호출, 에러 처리, 로딩 상태 관리 등이 필요합니다.향후 구현 시 다음 항목들을 고려해 주세요:
- 프로필 업데이트 API 호출
- 로딩/에러 상태 관리 (React Query mutation 활용)
- 성공/실패 토스트 메시지
- 낙관적 UI 업데이트 (useOptimistic 활용 가능)
API 연동을 위한 mutation hook 구조를 생성해 드릴까요?
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
64-76:onChange핸들러 패턴을 개선할 수 있습니다.Line 69-72의
onChange핸들러에서register("nickname").onChange를 수동으로 호출하는 것은 비표준 패턴입니다.{...register("nickname")}가 이미onChange를 포함하고 있으므로 중복될 수 있습니다.react-hook-form의
watch를 사용하는 것이 더 명확합니다:// 컴포넌트 상단에서 const nickname = watch("nickname"); useEffect(() => { if (nickname) { onNicknameChange({ target: { value: nickname } } as any); } }, [nickname, onNicknameChange]); // Input에서는 <Input label="닉네임" variant="filled" {...register("nickname")} defaultValue={defaultNickname} // onChange 제거 placeholder="정조이" isError={!!errors.nickname} errorMessage={errors.nickname?.message} />또는 커스텀 onChange가 꼭 필요하다면:
<Input {...register("nickname", { onChange: (e) => onNicknameChange(e) })} // 나머지 props... />apps/web/src/domains/user/domain/entities/user.entity.ts (1)
59-60: 신체 정보 값에 대한 유효성 검증을 추가하는 것을 고려하세요.생성자에서
height와weight값을 검증 없이 그대로 할당하고 있습니다. 음수, 0, 또는 비정상적으로 큰 값(예: 키 300cm, 몸무게 500kg)이 엔티티에 저장될 수 있습니다. 엔티티 레벨에서 기본적인 범위 검증을 추가하면 데이터 무결성을 보장할 수 있습니다.생성자에 다음과 같은 검증 로직 추가를 고려하세요:
constructor(params: { id: number; nickname: string; email?: string; profileImageUrl?: string; height?: number; weight?: number; createdAt?: string; updatedAt?: string; }) { // 기존 검증... if (params.height !== undefined && (params.height <= 0 || params.height > 300)) { throw new Error("Height must be between 0 and 300 cm"); } if (params.weight !== undefined && (params.weight <= 0 || params.weight > 500)) { throw new Error("Weight must be between 0 and 500 kg"); } // 필드 할당... }Also applies to: 75-76
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
apps/web/public/images/intern-nuguri.pngis excluded by!**/*.png
📒 Files selected for processing (18)
apps/web/app/(auth)/profile/edit/page.tsx(1 hunks)apps/web/src/domains/user/domain/entities/user.entity.ts(6 hunks)apps/web/src/domains/user/presentation/adapters/user.adapter.ts(2 hunks)apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts(1 hunks)apps/web/src/domains/user/presentation/types/user.type.ts(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-points-balance/index.css.ts(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-points-balance/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/sections/user-points-history-section/index.css.ts(1 hunks)apps/web/src/domains/user/presentation/ui/sections/user-points-history-section/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.css.ts(2 hunks)apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.tsx(4 hunks)apps/web/src/domains/user/presentation/ui/views/user-points-view/index.css.ts(1 hunks)apps/web/src/domains/user/presentation/ui/views/user-points-view/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/views/user-profile-view/index.tsx(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx (2)
apps/web/src/core/ui/components/nav-bar/index.tsx (1)
NavBar(16-48)apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx (1)
UserProfileEditFormSectionMock(21-69)
apps/web/src/domains/user/presentation/ui/sections/user-points-history-section/index.css.ts (2)
apps/web/src/domains/user/presentation/ui/components/user-points-balance/index.css.ts (1)
container(3-7)apps/web/src/domains/user/presentation/ui/views/user-points-view/index.css.ts (1)
container(4-7)
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts (1)
ProfileEditFormData(19-19)
apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx (3)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts (2)
ProfileEditFormData(19-19)profileEditSchema(3-17)apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
UserBasicInfoFormComponent(16-37)apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx (1)
UserPhysicalInfoFormComponent(13-30)
apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx (1)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts (1)
ProfileEditFormData(19-19)
apps/web/src/domains/user/presentation/adapters/user.adapter.ts (2)
apps/web/src/domains/user/domain/entities/user.entity.ts (1)
UserProfile(5-42)apps/web/src/domains/user/presentation/types/user.type.ts (1)
UserProfileItem(1-10)
apps/web/src/domains/user/presentation/ui/views/user-profile-view/index.tsx (2)
apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.tsx (1)
UserProfileSection(15-23)apps/web/src/domains/user/presentation/ui/sections/user-profile-point-section/index.tsx (1)
UserProfilePointSection(7-26)
apps/web/app/(auth)/profile/edit/page.tsx (1)
apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx (1)
UserProfileEditView(5-12)
apps/web/src/domains/user/presentation/ui/views/user-points-view/index.tsx (2)
apps/web/src/core/ui/components/nav-bar/index.tsx (1)
NavBar(16-48)apps/web/src/domains/user/presentation/ui/sections/user-points-balance-section/index.tsx (1)
UserPointsBalanceSection(5-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy
🔇 Additional comments (24)
apps/web/src/domains/user/presentation/ui/components/user-points-balance/index.tsx (1)
20-20: 레이아웃 변경사항 확인됨VStack에
pX={16}수평 패딩이 추가되어 일관된 레이아웃을 제공합니다. 관련 CSS 파일에서boxSizing: "border-box"가 추가되어 패딩이 전체 너비 계산에 올바르게 포함됩니다.apps/web/src/domains/user/presentation/ui/components/user-points-balance/index.css.ts (1)
6-6: 적절한 box-sizing 설정
boxSizing: "border-box"추가로 패딩과 테두리가 요소의 전체 너비/높이에 포함되어 예측 가능한 레이아웃을 보장합니다. 컴포넌트에 추가된 수평 패딩과 잘 조화됩니다.apps/web/app/(auth)/profile/edit/page.tsx (1)
1-7: 프로필 수정 페이지 구현 확인페이지 구조가 깔끔하고 뷰 컴포넌트로 적절히 위임하고 있습니다. PR 설명에 따르면 현재는 UI만 구현된 상태이며 서버 DTO 및 API가 아직 준비되지 않았습니다. 향후 실제 데이터 연동 시 repository 레이어와 API 통합이 필요합니다.
서버 통합 작업이 예정되어 있는지 확인하세요. 현재는 프레젠테이션 레이어만 구현되어 있어 실제 프로필 수정 기능이 동작하지 않습니다.
apps/web/src/domains/user/presentation/ui/views/user-points-view/index.css.ts (1)
6-6: 배경색 추가 확인컨테이너에 테마 변수를 사용한 배경색이 추가되어 일관된 UI를 제공합니다. 기존
boxSizing: "border-box"와 함께 적절한 스타일링이 적용되었습니다.apps/web/src/domains/user/presentation/ui/sections/user-points-history-section/index.tsx (2)
8-8: 스타일링 통합 확인CSS 모듈을 import하고 VStack에 적절한 레이아웃 속성(
pY={16},grow={1},className)을 추가하여 일관된 UI를 구현했습니다.Also applies to: 15-15
11-11: 향후 API 통합 필요TODO 코멘트와 mock 데이터 훅이 존재합니다. 현재는 UI 레이어만 구현된 상태로, 향후 실제 API 호출로 대체가 필요합니다.
실제 포인트 히스토리 데이터를 가져오는 API 엔드포인트가 준비되면
usePointsData훅을 실제 데이터 페칭 로직으로 교체해야 합니다.Also applies to: 22-41
apps/web/src/domains/user/presentation/ui/views/user-profile-view/index.tsx (1)
2-2: 레이아웃 구조 개선
UserProfileSection과UserProfilePointSection을VStack으로 감싸 수직 레이아웃을 명확하게 구성했습니다. 로직 변경 없이 순수하게 프레젠테이션 레이어만 개선되었습니다.Also applies to: 13-16
apps/web/src/domains/user/presentation/types/user.type.ts (1)
6-7: UserProfileItem 인터페이스 확장
height와weight필드가 옵셔널 속성으로 추가되어 프로필 수정 기능을 지원합니다. 모든 사용자가 이 데이터를 제공하지 않을 수 있으므로 옵셔널로 정의한 것이 적절합니다. 관련 파일들(entity, adapter, schema)에서도 일관되게 이 필드들을 처리하고 있습니다.apps/web/src/domains/user/presentation/ui/sections/user-points-history-section/index.css.ts (1)
1-6: 새 CSS 모듈 구현포인트 히스토리 섹션을 위한 새로운 스타일 모듈이 추가되었습니다. 테마 변수를 사용하여 배경색을 설정하고 있어 일관된 디자인 시스템을 유지합니다.
apps/web/src/domains/user/presentation/ui/views/user-points-view/index.tsx (1)
18-19: 레이아웃 구조 개선이 잘 적용되었습니다.불필요한 중첩 VStack을 제거하여 컴포넌트 구조가 더 명확해졌습니다. NavBar의
background="transparent"속성도 UI 요구사항에 적합하게 설정되었습니다.apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.css.ts (3)
7-8: 테마 토큰 사용이 잘 적용되었습니다.고정 픽셀 값 대신
vars.box.spacing[4]를 사용하여 일관된 디자인 시스템을 유지하고 있습니다.
14-14:objectFit변경 의도를 확인해 주세요.
"cover"에서"contain"으로 변경하면 이미지가 잘리지 않지만, 컨테이너를 완전히 채우지 못할 수 있습니다. 이것이 의도된 디자인인지 확인이 필요합니다.
34-51: 클릭 영역 확장 패턴이 잘 구현되었습니다.
::before가상 요소를 사용하여 작은 아이콘의 클릭 영역을 확장한 것은 모바일 접근성 향상에 도움이 됩니다.inset: "-12px"를 통해 최소 44x44px의 터치 타겟 크기를 확보하고 있습니다.apps/web/src/domains/user/presentation/ui/views/user-profile-edit-view/index.tsx (1)
1-12: 프로필 수정 뷰가 깔끔하게 구현되었습니다.프레젠테이션 레이어 컴포넌트로서 역할이 명확하고,
minH="100dvh"를 사용하여 모바일 브라우저의 동적 뷰포트 높이를 올바르게 처리하고 있습니다.apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx (2)
15-19: 목 데이터가 적절하게 구성되었습니다.개발 단계에서 UI 테스트를 위한 목 데이터로 적합합니다.
47-54: 닉네임 검증 핸들러가 구현되지 않았습니다.Line 51의 닉네임 중복 체크와 Line 52의 onChange 핸들러가 구현되지 않아 실제 검증이 동작하지 않습니다.
현재는 목 구현이지만, 실제 구현 시:
- 닉네임 변경 시 중복 체크 버튼 활성화
- API를 통한 중복 검증
- 검증 결과에 따른 에러 표시
이러한 로직이 필요합니다. 이 부분의 구현 계획을 확인해 주세요.
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
1-108: 폼 컴포넌트가 잘 구성되었습니다.컴포넌트 분리와 타입 정의가 명확하며, 에러 처리와 로딩 상태 관리가 적절하게 구현되어 있습니다.
apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.tsx (2)
29-41: 스켈레톤 UI가 개선되었습니다.실제 콘텐츠 구조와 더 유사한 스켈레톤을 제공하여 레이아웃 이동(CLS)을 최소화하고 있습니다.
57-72: 하드코딩된 값들을 확인해 주세요."Lv.1", "인턴 너구리", "손님" 등의 값이 하드코딩되어 있습니다. 이 값들이 실제 데이터로 교체될 예정인지 확인이 필요합니다.
다음 사항들을 확인해 주세요:
- 레벨 정보는 API에서 제공될 예정인가요?
- "인턴 너구리"는 고정된 타이틀인가요, 아니면 레벨에 따라 변경되나요?
- 사용자 역할/타이틀 정보가
UserProfileItem에 추가되어야 하나요?apps/web/src/domains/user/presentation/adapters/user.adapter.ts (2)
11-12: UI 아이템에 신체 정보 필드가 올바르게 추가되었습니다.엔티티의
getHeight()와getWeight()메서드를 통해 안전하게 값을 가져오고 있습니다.
53-62: 프로필 완성도 계산에서 신체 정보를 제외한 것이 의도된 것인지 확인해 주세요.
getProfileCompletionPercentage메서드가 nickname, email, profileImage만 카운트하고 height/weight는 고려하지 않습니다. 이것이 의도된 설계인지 확인이 필요합니다.만약 신체 정보도 프로필 완성도에 포함되어야 한다면:
getProfileCompletionPercentage(profile: UserProfile): number { let completedFields = 0; const totalFields = 5; // nickname, email, profileImage, height, weight if (profile.getNickname()) completedFields++; if (profile.getEmail()) completedFields++; if (profile.getProfileImageUrl()) completedFields++; if (profile.getHeight()) completedFields++; if (profile.getWeight()) completedFields++; return Math.round((completedFields / totalFields) * 100); }또는 필수/선택 필드를 구분하여 계산할 수도 있습니다.
apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx (1)
6-30: 컴포넌트 구조가 잘 구성되어 있습니다.Props 인터페이스가 명확하고, 컴포넌트 분해가 적절하게 이루어졌습니다.
apps/web/src/domains/user/domain/entities/user.entity.ts (2)
11-12: 인터페이스 확장이 적절합니다.
getHeight()와getWeight()메서드 시그니처가 다른 선택적 필드들과 일관성 있게 정의되었습니다.
98-104: getter 구현이 올바릅니다.
getHeight()와getWeight()메서드가 private 필드를 올바르게 반환하고 있습니다.
| {...register("height", { | ||
| setValueAs: (v) => (v === "" ? undefined : Number(v)), | ||
| })} |
There was a problem hiding this comment.
🧩 Analysis chain
신체 정보 입력값의 범위 검증을 확인하세요.
현재 type="number"를 사용하지만, 음수나 비정상적으로 큰 값(예: 키 999cm, 몸무게 -10kg)을 방지하는 클라이언트 측 검증이 보이지 않습니다. ProfileEditFormData 스키마에 적절한 min/max 검증이 포함되어 있는지 확인하시기 바랍니다.
다음 스크립트를 실행하여 스키마에 검증 로직이 있는지 확인하세요:
Also applies to: 119-121
🏁 Script executed:
#!/bin/bash
# Description: profile-edit.schema.ts의 height와 weight 검증 규칙 확인
# profile-edit.schema.ts 파일에서 height와 weight의 검증 로직 찾기
rg -A 10 -B 2 'height|weight' apps/web/src/domains/user/presentation/schemas/profile-edit.schema.tsLength of output: 185
신체 정보 입력값의 범위 검증이 누락되어 있습니다.
스키마 검증 결과, height와 weight가 z.number().optional()으로만 정의되어 있으며, 음수, 0, 또는 비정상적으로 큰 값(예: 키 999cm, 몸무게 -10kg)을 방지하는 min/max 제약이 없습니다.
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts에서 다음과 같이 범위 검증을 추가하세요:
height: z.number().min(50).max(300).optional(),
weight: z.number().min(10).max(500).optional(),
적절한 범위값은 프로젝트의 요구사항에 맞게 조정하시기 바랍니다.
Also applies to: 119-121
🤖 Prompt for AI Agents
In
apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx
around lines 92-94 and 119-121, the schema validation for height and weight
lacks range checks; update the corresponding schema definitions in
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts by
replacing z.number().optional() for height and weight with
z.number().min(50).max(300).optional() and
z.number().min(10).max(500).optional() respectively (or adjust the min/max to
match project requirements), ensuring the form parsing still maps empty strings
to undefined.
setValueAs에서 NaN 처리가 누락되었습니다.
빈 문자열은 undefined로 변환하지만, 유효하지 않은 숫자 입력(예: 공백, 문자 등)은 Number(v)가 NaN을 반환합니다. NaN은 서버로 전송되거나 유효성 검사를 우회할 수 있습니다.
다음 diff를 적용하여 NaN 처리를 추가하세요:
{...register("height", {
- setValueAs: (v) => (v === "" ? undefined : Number(v)),
+ setValueAs: (v) => {
+ if (v === "" || v == null) return undefined;
+ const num = Number(v);
+ return Number.isNaN(num) ? undefined : num;
+ },
})}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {...register("height", { | |
| setValueAs: (v) => (v === "" ? undefined : Number(v)), | |
| })} | |
| {...register("height", { | |
| setValueAs: (v) => { | |
| if (v === "" || v == null) return undefined; | |
| const num = Number(v); | |
| return Number.isNaN(num) ? undefined : num; | |
| }, | |
| })} |
🤖 Prompt for AI Agents
In
apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx
around lines 92-94, the setValueAs handler currently converts empty string to
undefined but calls Number(v) for other inputs which can produce NaN for invalid
numeric input; update setValueAs to parse the value into a number, then return
undefined when the parsed result is NaN (e.g., const n = Number(v); return
isNaN(n) ? undefined : n) so invalid inputs don’t become NaN and aren’t sent to
the server or bypass validation.
There was a problem hiding this comment.
height: z.number().min(50).max(300).optional(),
weight: z.number().min(10).max(500).optional(),
해당 부분은 아직 고민인 부분입니다. 키와 몸무게를 과연 적당한 min 값을 정해야한다고 생각하지만, 적당한 값을 정하지 못했습니다
일단 제안한 방법대로 변경해보고 의미 있는 값으로 설정해보도록하겠습니다.
There was a problem hiding this comment.
@hijjoy, 좋은 접근 방식입니다! 사용자의 실제 데이터를 고려하여 적절한 범위를 설정하는 것이 중요하겠네요.
참고로, 일반적인 범위 설정 시 고려사항:
- 키(height): 일반적으로 성인 기준 최소 130-140cm, 최대 210-220cm 정도가 합리적입니다
- 몸무게(weight): 최소 30-40kg, 최대 200-250kg 정도를 고려할 수 있습니다
서비스의 타겟 사용자층과 실제 사용 케이스를 반영하여 결정하시면 됩니다. 추가로 NaN 처리 로직도 함께 적용해주시면 더욱 안전한 구현이 될 것 같습니다. 👍
There was a problem hiding this comment.
setValueAs 정리
setValueAs는 react-hook-form에서 폼 값을 최종 저장하기 직전에 변환하는 함수
실행 시점: 사용자가 입력할 때마다 (onChange), getValues(), handleSubmit() 호출 시 등..
- HTML input의 값은 항상 string입니다 - type="number"여도 e.target.value는 문자열로 반환
- Zod 스키마는 number 타입을 기대합니다: height: z.number().min(130, "...").max(220, "...").optional()
- 빈 값 처리 - 사용자가 input을 비우면 "" (빈 문자열)이 되는데, 이를 undefined로 변환해야 optional 필드가 제대로 작동합니다.
- 잘못된 입력 처리 - 사용자가 숫자가 아닌 값을 입력하면 Number(v)가 NaN을 반환하는데, 이를 undefined로 처리해야 validation이 올바르게 동작합니다.
There was a problem hiding this comment.
@hijjoy, 정확히 이해하셨습니다! 👍
설명해주신 setValueAs의 동작 방식과 네 가지 핵심 포인트가 모두 정확합니다. 특히 다음 두 가지가 중요합니다:
- 빈 값 →
undefined: optional 필드가 제대로 동작하려면 필수적 - NaN →
undefined: 잘못된 입력이 validation을 우회하는 것을 방지
제안드린 코드 패턴은 이 두 케이스를 모두 안전하게 처리합니다:
setValueAs: (v) => {
if (v === "" || v == null) return undefined;
const num = Number(v);
return Number.isNaN(num) ? undefined : num;
}height와 weight 범위값과 함께 적용해주시면 안전한 폼 구현이 완성될 것 같습니다! 🙂
✏️ Learnings added
Learnt from: hijjoy
Repo: Nugudi/websites PR: 215
File: apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx:92-98
Timestamp: 2025-11-11T05:19:24.613Z
Learning: In React Hook Form with Zod validation for optional numeric fields (like height, weight), use setValueAs to safely convert string inputs to numbers by checking for empty strings, null, and NaN, and returning undefined for all invalid cases. This ensures optional fields work correctly and invalid inputs don't bypass validation. Pattern: setValueAs: (v) => { if (v === "" || v == null) return undefined; const num = Number(v); return Number.isNaN(num) ? undefined : num; }
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
.../web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx
Show resolved
Hide resolved
| <Badge tone="positive" size="xs" variant="weak"> | ||
| Lv.1 | ||
| </Badge> | ||
| <Flex direction="column" gap={4}> | ||
| <Title fontSize="t3" color="main" colorShade={800}> | ||
| 인턴 너구리 | ||
| </Title> | ||
| <HStack gap={4} align="center"> | ||
| <Body fontSize="b3b" colorShade={700}> | ||
| {nickname} | ||
| </Body> | ||
| <Link href="/profile/edit" className={styles.editButton}> | ||
| <PencilIcon width={16} height={16} /> | ||
| </Link> | ||
| </HStack> | ||
| </Flex> |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
에러 상태와 콘텐츠 상태의 UI가 중복됩니다.
Line 57-72의 에러 UI와 Line 96-111의 실제 콘텐츠 UI가 거의 동일한 구조입니다. 공통 컴포넌트로 추출하여 코드 중복을 제거할 수 있습니다.
다음과 같이 공통 컴포넌트로 추출할 수 있습니다:
interface UserProfileDisplayProps {
profileImageUrl: string;
nickname: string;
level?: string;
title?: string;
}
const UserProfileDisplay = ({
profileImageUrl,
nickname,
level = "Lv.1",
title = "인턴 너구리"
}: UserProfileDisplayProps) => {
return (
<Flex className={styles.container}>
<Image
priority
src={profileImageUrl}
alt="profile"
width={165}
height={130}
className={styles.profileImage}
/>
<Flex className={styles.infoWrapper}>
<Badge tone="positive" size="xs" variant="weak">
{level}
</Badge>
<Flex direction="column" gap={4}>
<Title fontSize="t3" color="main" colorShade={800}>
{title}
</Title>
<HStack gap={4} align="center">
<Body fontSize="b3b" colorShade={700}>
{nickname}
</Body>
<Link href="/profile/edit" className={styles.editButton}>
<PencilIcon width={16} height={16} />
</Link>
</HStack>
</Flex>
</Flex>
</Flex>
);
};
// 그리고 각 컴포넌트에서:
const UserProfileSectionError = () => (
<UserProfileDisplay
profileImageUrl="/images/intern-nuguri.png"
nickname="손님"
/>
);
const UserProfileSectionContent = () => {
const { data: profileData } = useSuspenseQuery(userProfileQueryClient);
return (
<UserProfileDisplay
profileImageUrl={profileData.profileImageUrl ?? "/images/intern-nuguri.png"}
nickname={profileData.nickname ?? "손님"}
/>
);
};🤖 Prompt for AI Agents
In
apps/web/src/domains/user/presentation/ui/sections/user-profile-section/index.tsx
around lines 57-72 and 96-111, the error-state UI and the real-content UI are
duplicated; extract a shared presentational component (e.g., UserProfileDisplay)
that accepts profileImageUrl, nickname, level, and title props and use it for
both the error and content variants; update the error variant to pass default
placeholder values and the content variant to pass values from profileData (with
fallbacks), and replace the duplicated JSX blocks with calls to the new shared
component while keeping existing className/style/props intact.
There was a problem hiding this comment.
현재 브랜치는 프로필 수정 컴포넌트와 관련되어있으므로, 이 부분은 추후 실제로 프로필 연결 과정에서 수정하도록 하겟습니다.
There was a problem hiding this comment.
@hijjoy, 이해했습니다! 현재 PR의 범위를 고려하면 합리적인 결정입니다.
추후 프로필 연결 작업 시 이 리팩토링을 잊지 않도록 후속 이슈를 생성하시겠습니까? 원하신다면 제가 이슈를 만들어 드릴 수 있습니다.
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (1)
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
62-66: onChange 핸들러 패턴을 더 간결하게 개선할 수 있습니다.현재 패턴은 올바르게 동작하지만, spread 연산자로 포함된 onChange를 오버라이드한 후 수동으로 다시 호출하는 방식이 다소 장황합니다. 구조 분해를 사용하면 더 명확하게 표현할 수 있습니다.
다음 diff를 적용하여 패턴을 개선하세요:
+ const { onChange, ...registration } = register("nickname"); <Input label="닉네임" variant="filled" - {...register("nickname")} + {...registration} onChange={(e) => { - register("nickname").onChange(e); + onChange(e); onNicknameChange(e); }} placeholder="정조이" isError={!!errors.nickname} errorMessage={errors.nickname?.message} />
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx(1 hunks)apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- apps/web/src/domains/user/presentation/ui/sections/user-profile-edit-form-section-mock/index.tsx
- apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts
- apps/web/src/domains/user/presentation/ui/components/user-physical-info-form-component/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
apps/web/src/domains/user/presentation/schemas/profile-edit.schema.ts (1)
ProfileEditFormData(27-27)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: deploy
🔇 Additional comments (1)
apps/web/src/domains/user/presentation/ui/components/user-basic-info-form-component/index.tsx (1)
86-102: 잘 구현되었습니다!중복 체크 버튼이 올바르게 구현되었습니다.
type="button"으로 폼 제출을 방지하고, 로딩 상태를isLoading과disabled속성에 모두 연결하여 적절한 사용자 경험을 제공합니다.
|
This pull request (commit
|
- 사용자 엔티티와 어댑터에 키·몸무게 속성 추가 - 프로필 수정 폼 스키마와 기본 정보/신체 정보 컴포넌트 구현 - 프로필 수정 페이지에서 신규 뷰 컴포넌트 렌더링
- 인턴 너구리 이미지를 추가하고 기본 프로필 이미지를 교체 - 배지와 타이포그래피 컴포넌트로 프로필 정보 표현을 재구성 - 프로필 뷰에 스택 레이아웃을 적용해 섹션 배치를 정리했습니다 - 수정 버튼 범위 클릭 범위 확장
- navbar 변경으로 인한 깨진 UI 수정
- 키/몸무게 필드에 범위 검증 추가 (키: 130-220cm, 몸무게: 30-250kg) - Number.isNaN() 사용하여 타입 안정성 강화 - 빈 값 및 잘못된 입력 처리를 위한 setValueAs 핸들러 개선 - react-hook-form defaultValues와 중복되는 defaultValue props 제거
bc783a8 to
1aee6e2
Compare
|
This pull request (commit
|
|
@dydals3440 이 UI만 마지막으로 마무리하고(아직 dto없는 화면), 이제 클린아키텍쳐에 맞춰 api 나온 화면들 연결하려고합니다 🐿️ |
고생하셨습니당 팟탱 나머지도.. |
🌀 Issue Number
😵 As-Is
🥳 To-Be
85ef631 : 프로필 수정 페이지 구현
858a3e4 , a48ed2b : 두 부분은 이전에 있던 UI를 바탕으로 css만 수정하였습니다.
✅ Check List
📷 Test Screenshot (Optional)
👾 Additional Description (Optional)
Summary by CodeRabbit